using System;
using System.Linq;
using Habanero.Base;
using Habanero.BO;

namespace FireStarter.Base.BusinessObjectMerger
{
    public class BusinessObjectMerger
    {
        private IMergableBo _existingMergableBo;
        private readonly Predicate<IBOProp> _ignorePropertyDelegate;
        private readonly BusinessObjectRelationshipTraverser _businessObjectRelationshipTraverser;
        private BoComparer _boComparer;

        public BusinessObjectMerger(IMergableBo existingDatabaseInfo, Predicate<IBOProp> ignorePropertyDelegate)
        {
            _existingMergableBo = existingDatabaseInfo;
            _ignorePropertyDelegate = ignorePropertyDelegate;
            _businessObjectRelationshipTraverser = new BusinessObjectRelationshipTraverser();
            _boComparer = new BoComparer(MustIgnoreProperty);
        }

        public IMergableBo ExistingMergableBo
        {
            get { return _existingMergableBo; }
        }

        public void Import(IMergableBo newMergableBo)
        {
            if (_existingMergableBo == null)
            {
                if (newMergableBo == null) return;
                newMergableBo.PendingChangeType = ChangeType.Add;
                _existingMergableBo = newMergableBo;
            }
            MergeBusinessObjects(newMergableBo, _existingMergableBo);
        }

        private void MergeBusinessObjects(IMergableBo newMergableBo, IMergableBo existingMergableBo)
        {
            if (newMergableBo == null)
            {
                if (existingMergableBo == null) return;
                existingMergableBo.PendingChangeType = ChangeType.Delete;
                return;
            }
            // check to see if existing columns have changed
            if (!_boComparer.Equals(existingMergableBo, newMergableBo))
            {
                CopyProperties(newMergableBo, existingMergableBo);
                // Mark existingDB + Column as Updated
                existingMergableBo.PendingChangeType = ChangeType.Update;
            }
            _businessObjectRelationshipTraverser.TraverseChildren(existingMergableBo,
                                                                  delegate(IBusinessObject businessObject, IRelationship relationship)
                                                                      {
                                                                          if ((relationship is IMultipleRelationship))
                                                                          {
                                                                              IMultipleRelationship existingBoMultipleRelationship = (IMultipleRelationship)relationship;
                                                                              MergeMultipleRelationship(newMergableBo, existingMergableBo, existingBoMultipleRelationship);
                                                                          }
                                                                          if ((relationship is ISingleRelationship))
                                                                          {
                                                                              ISingleRelationship existingBoSingleRelationship = (ISingleRelationship)relationship;
                                                                              MergeSingleRelationship(newMergableBo, existingMergableBo, existingBoSingleRelationship);
                                                                          }
                                                                      });
        }

        private void CopyProperties(IBusinessObject fromMergableBo, IBusinessObject toMergableBo)
        {
            foreach (IBOProp prop in fromMergableBo.Props)
            {
                // Update Properties of existing columnsInfo to match actual Column props 
                if (MustIgnoreProperty(prop)) continue;
                IBOProp existingProp = toMergableBo.Props[prop.PropertyName];
                existingProp.Value = prop.Value;
            }
        }

        protected virtual bool MustIgnoreProperty(IBOProp prop)
        {
            return prop.PropertyType == typeof(Guid) || prop.PropertyType == typeof(ChangeType) 
                || (_ignorePropertyDelegate != null && _ignorePropertyDelegate(prop));
        }

        private void MergeSingleRelationship(IMergableBo newMergableBo, IMergableBo existingMergableBo, ISingleRelationship existingBoSingleRelationship)
        {
            IRelationship newBoRelationship = newMergableBo.Relationships[existingBoSingleRelationship.RelationshipName];
            ISingleRelationship newBoSingleRelationship = (ISingleRelationship)newBoRelationship;
            IMergableBo existingRelatedBo = (IMergableBo) existingBoSingleRelationship.GetRelatedObject();
            IMergableBo newRelatedBo = (IMergableBo) newBoSingleRelationship.GetRelatedObject();
            //TODO: if one of these objects are null? Write test to Add the object, or Delete it.

            if (existingRelatedBo == null && newRelatedBo != null)
            {
                newRelatedBo.PendingChangeType = ChangeType.Add;
                existingRelatedBo = newRelatedBo;
                existingBoSingleRelationship.SetRelatedObject(existingRelatedBo);
            } else
            {
                MergeBusinessObjects(newRelatedBo, existingRelatedBo);
            }
            if (existingMergableBo.PendingChangeType == ChangeType.None)
            {
                if (existingRelatedBo != null && existingRelatedBo.PendingChangeType != ChangeType.None)
                {
                    existingMergableBo.PendingChangeType = ChangeType.Update;
                }
            }
        }
        
        private void MergeMultipleRelationship(IMergableBo newMergableBo, IMergableBo existingMergableBo, IMultipleRelationship existingBoMultipleRelationship)
        {
            IRelationship newBoRelationship = newMergableBo.Relationships[existingBoMultipleRelationship.RelationshipName];
            IMultipleRelationship newBoMultipleRelationship = (IMultipleRelationship)newBoRelationship;
            IBusinessObjectCollection existingBoCol = existingBoMultipleRelationship.BusinessObjectCollection;
            IBusinessObjectCollection newBoCol = newBoMultipleRelationship.BusinessObjectCollection;

            MergeCollections(newBoCol, existingBoCol);

            UpdateParentChangeType(existingMergableBo, existingBoCol);
        }

        private static void UpdateParentChangeType(IMergableBo parentMergableBo, IBusinessObjectCollection childBoCol)
        {
            if (parentMergableBo.PendingChangeType == ChangeType.None)
            {
                foreach (IBusinessObject businessObject in childBoCol)
                {
                    //if (!(businessObject is IMergableBo)) continue;
                    IMergableBo mergableBo = (IMergableBo)businessObject;
                    if (mergableBo.PendingChangeType != ChangeType.None)
                    {
                        parentMergableBo.PendingChangeType = ChangeType.Update;
                        break;
                    }
                }
            }
        }

        private void MergeCollections(IBusinessObjectCollection newBoCol, IBusinessObjectCollection existingBoCol)
        {
            MarkDeletedObjects(newBoCol, existingBoCol);
            AddAndUpdateObjects(existingBoCol, newBoCol);
        }

        private void AddAndUpdateObjects(IBusinessObjectCollection existingBoCol, IBusinessObjectCollection newBoCol)
        {
            foreach (IBusinessObject businessObject in newBoCol.Clone())
            {
                //if (!(businessObject is IMergableBo)) continue;
                IMergableBo mergableBo = (IMergableBo)businessObject;
                IMergableBo foundExistingMergableBo = FindBusinessObject(existingBoCol, mergableBo);
                if (foundExistingMergableBo == null)
                {
                    mergableBo.PendingChangeType = ChangeType.Add;
                    existingBoCol.Add(mergableBo);
                    //TODO: check to see that tableinfo change type is none before changing to update 
                    UpdateChildChangeTypes(mergableBo, ChangeType.Add);
                }
                else
                {
                    MergeBusinessObjects(mergableBo, foundExistingMergableBo);
                }
            }
        }

        private void UpdateChildChangeTypes(IMergableBo mergableBo, ChangeType changeType)
        {

            _businessObjectRelationshipTraverser.TraverseChildren(mergableBo,
                                                                  delegate(IBusinessObject businessObject, IRelationship relationship)
                                                                      {
                                                                          if ((relationship is IMultipleRelationship))
                                                                          {
                                                                              IMultipleRelationship existingBoMultipleRelationship = (IMultipleRelationship)relationship;
                                                                              foreach (IBusinessObject bo in existingBoMultipleRelationship.BusinessObjectCollection)
                                                                              {
                                                                                  IMergableBo relatedObject = bo as IMergableBo;
                                                                                  if (relatedObject != null)
                                                                                  {
                                                                                      relatedObject.PendingChangeType = changeType;
                                                                                      UpdateChildChangeTypes(relatedObject, changeType);
                                                                                  }
                                                                              }
                                                                          }
                                                                          if ((relationship is ISingleRelationship))
                                                                          {
                                                                              ISingleRelationship existingBoSingleRelationship = (ISingleRelationship) relationship;
                                                                              IMergableBo relatedObject = existingBoSingleRelationship.GetRelatedObject() as IMergableBo;
                                                                              if (relatedObject != null)
                                                                              {
                                                                                  relatedObject.PendingChangeType = changeType;
                                                                                  UpdateChildChangeTypes(relatedObject, changeType);
                                                                              }
                                                                          }
                                                                      });
        }

        private static void MarkDeletedObjects(IBusinessObjectCollection newBoCol, IBusinessObjectCollection existingBoCol)
        {
            foreach (IBusinessObject businessObject in existingBoCol.Clone())
            {
                //if (!(businessObject is IMergableBo)) continue;
                IMergableBo mergableBo = (IMergableBo)businessObject;
                IMergableBo foundMergableBo = FindBusinessObject(newBoCol, mergableBo);
                if (foundMergableBo == null)
                {
                    mergableBo.PendingChangeType = ChangeType.Delete;
                }
            }
        }

        private static IMergableBo FindBusinessObject(IBusinessObjectCollection businessObjectCollection, IMergableBo mergableBoToFind)
        {
            return businessObjectCollection.Clone().Cast<IMergableBo>().FirstOrDefault(mergableBo => mergableBo.ToString() == mergableBoToFind.ToString());
        }
    }
}